1 /** 2 Copyright: Copyright (c) 2021, Joakim Brännström. All rights reserved. 3 License: MPL-2 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 6 This Source Code Form is subject to the terms of the Mozilla Public License, 7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain 8 one at http://mozilla.org/MPL/2.0/. 9 10 Code copied from dextool 11 */ 12 module code_checker.database; 13 14 import logger = std.experimental.logger; 15 import std.algorithm : map, joiner, filter; 16 import std.array : appender, array, empty; 17 import std.datetime : SysTime, Duration; 18 import std.exception : collectException; 19 import std.format : format; 20 import std.typecons : Nullable, Flag, No; 21 22 import miniorm : Miniorm, select, insert, insertOrReplace, delete_, 23 insertOrIgnore, toSqliteDateTime, fromSqLiteDateTime, Bind; 24 import my.named_type; 25 import my.optional; 26 import my.path; 27 import my.hash : Checksum64; 28 29 import code_checker.database.schema; 30 31 /** Database wrapper with minimal dependencies. 32 */ 33 struct Database { 34 package Miniorm db; 35 36 /** Create a database by either opening an existing or initializing a new. 37 * 38 * Params: 39 * db = path to the database 40 */ 41 static auto make(AbsolutePath db) @safe { 42 return Database(initializeDB(db)); 43 } 44 45 auto transaction() @trusted { 46 return db.transaction; 47 } 48 49 DbDependency dependencyApi() return @safe { 50 return typeof(return)(&db, &this); 51 } 52 53 DbFile fileApi() return @safe { 54 return typeof(return)(&db, &this); 55 } 56 57 DbCompileDbTrack compileDbTrackApi() return @safe { 58 return typeof(return)(&db, &this); 59 } 60 } 61 62 struct DbFile { 63 private Miniorm* db; 64 private Database* wrapperDb; 65 66 void put(const Path p, Checksum64 cs, SysTime lastModified) @trusted { 67 static immutable sql = format!"INSERT INTO %s (path, checksum, root, time_stamp) 68 VALUES (:path, :checksum, 1, :ts) 69 ON CONFLICT (path) DO UPDATE SET checksum=:checksum,time_stamp=:ts"( 70 filesTable); 71 auto stmt = db.prepare(sql); 72 stmt.get.bind(":path", p.toString); 73 stmt.get.bind(":checksum", cast(long) cs.c0); 74 stmt.get.bind(":ts", lastModified.toSqliteDateTime); 75 stmt.get.execute; 76 } 77 78 /// Returns: the file path that the id correspond to. 79 Nullable!TrackFile getFile(const FileId id) @trusted { 80 static immutable sql = format( 81 "SELECT path,checksum,time_stamp FROM %s WHERE id = :id", filesTable); 82 auto stmt = db.prepare(sql); 83 stmt.get.bind(":id", id.get); 84 85 typeof(return) rval; 86 foreach (ref r; stmt.get.execute) 87 rval = TrackFile(Path(r.peek!string(0)), 88 Checksum64(r.peek!long(1)), r.peek!string(2).fromSqLiteDateTime); 89 return rval; 90 } 91 92 Nullable!TrackFile getFile(const Path path) @trusted { 93 static immutable sql = format( 94 "SELECT path,checksum,time_stamp FROM %s WHERE path=:path", filesTable); 95 auto stmt = db.prepare(sql); 96 stmt.get.bind(":path", path); 97 98 typeof(return) rval; 99 foreach (ref r; stmt.get.execute) 100 rval = TrackFile(Path(r.peek!string(0)), 101 Checksum64(r.peek!long(1)), r.peek!string(2).fromSqLiteDateTime); 102 return rval; 103 } 104 105 Nullable!FileId getFileId(const Path p) @trusted { 106 static immutable sql = format("SELECT id FROM %s WHERE path=:path", filesTable); 107 auto stmt = db.prepare(sql); 108 stmt.get.bind(":path", p.toString); 109 auto res = stmt.get.execute; 110 111 typeof(return) rval; 112 if (!res.empty) 113 rval = FileId(res.oneValue!long); 114 return rval; 115 } 116 117 /// Remove the file with all mutations that are coupled to it. 118 void removeFile(const Path p) @trusted { 119 auto stmt = db.prepare(format!"DELETE FROM %s WHERE path=:path"(filesTable)); 120 stmt.get.bind(":path", p.toString); 121 stmt.get.execute; 122 } 123 124 /// Returns: all files tagged as a root. 125 FileId[] getRootFiles() @trusted { 126 static immutable sql = format!"SELECT id FROM %s WHERE root=1"(filesTable); 127 128 auto app = appender!(FileId[])(); 129 auto stmt = db.prepare(sql); 130 foreach (ref r; stmt.get.execute) { 131 app.put(r.peek!long(0).FileId); 132 } 133 return app.data; 134 } 135 136 /// Returns: All files in the database as relative paths. 137 Path[] getFiles() @trusted { 138 auto stmt = db.prepare(format!"SELECT path FROM %s"(filesTable)); 139 auto res = stmt.get.execute; 140 141 auto app = appender!(Path[]); 142 foreach (ref r; res) { 143 app.put(Path(r.peek!string(0))); 144 } 145 146 return app.data; 147 } 148 149 Nullable!Checksum64 getFileChecksum(const Path p) @trusted { 150 static immutable sql = format!"SELECT checksum FROM %s WHERE path=:path"(filesTable); 151 auto stmt = db.prepare(sql); 152 stmt.get.bind(":path", p.toString); 153 auto res = stmt.get.execute; 154 155 typeof(return) rval; 156 if (!res.empty) { 157 rval = Checksum64(res.front.peek!long(0)); 158 } 159 160 return rval; 161 } 162 } 163 164 /** Dependencies between root and those files that should trigger a re-analyze 165 * of the root if they are changed. 166 */ 167 struct DbDependency { 168 private Miniorm* db; 169 private Database* wrapperDb; 170 171 /// The root must already exist or the whole operation will fail with an sql error. 172 void set(const Path path, const DepFile[] deps) @trusted { 173 static immutable insertDepSql = "INSERT INTO " ~ depFileTable ~ " (file,checksum,time_stamp) 174 VALUES(:file,:cs,:ts) 175 ON CONFLICT (file) DO UPDATE SET checksum=:cs,time_stamp=:ts WHERE file=:file"; 176 177 auto stmt = db.prepare(insertDepSql); 178 auto ids = appender!(long[])(); 179 foreach (a; deps) { 180 stmt.get.bind(":file", a.file.toString); 181 stmt.get.bind(":cs", cast(long) a.checksum.c0); 182 stmt.get.bind(":ts", a.timeStamp.toSqliteDateTime); 183 stmt.get.execute; 184 stmt.get.reset; 185 186 // can't use lastInsertRowid because a conflict would not update 187 // the ID. 188 auto id = getId(a.file); 189 if (id.hasValue) 190 ids.put(id.orElse(0L)); 191 } 192 193 static immutable addRelSql = "INSERT OR IGNORE INTO " ~ depRootTable 194 ~ " (dep_id,file_id) VALUES(:did, :fid)"; 195 stmt = db.prepare(addRelSql); 196 const fid = () { 197 auto a = wrapperDb.fileApi.getFileId(path); 198 if (a.isNull) { 199 throw new Exception( 200 "File is not tracked (is missing from the files table in the database) " 201 ~ path); 202 } 203 return a.get; 204 }(); 205 206 foreach (id; ids.data) { 207 stmt.get.bind(":did", id); 208 stmt.get.bind(":fid", fid.get); 209 stmt.get.execute; 210 stmt.get.reset; 211 } 212 213 // remove dropped relations 214 stmt = db.prepare(format!"DELETE FROM %s WHERE file_id=:fid AND dep_id NOT IN (%(%s,%))"(depRootTable, 215 ids.data)); 216 stmt.get.bind(":fid", fid.get); 217 stmt.get.execute; 218 } 219 220 private Optional!long getId(const Path file) { 221 foreach (a; db.run(select!DependencyFileTable.where("file = :file", 222 Bind("file")), file.toString)) { 223 return some(a.id); 224 } 225 return none!long; 226 } 227 228 /// Returns: all dependencies. 229 DepFile[] getAll() @trusted { 230 return db.run(select!DependencyFileTable) 231 .map!(a => DepFile(Path(a.file), Checksum64(a.checksum), a.timeStamp)).array; 232 } 233 234 /// Returns: all files that a root is dependent on. 235 Path[] get(const Path root) @trusted { 236 static immutable sql = format!"SELECT t0.file 237 FROM %1$s t0, %2$s t1, %3$s t2 238 WHERE 239 t0.id = t1.dep_id AND 240 t1.file_id = t2.id AND 241 t2.path = :file"(depFileTable, 242 depRootTable, filesTable); 243 244 auto stmt = db.prepare(sql); 245 stmt.get.bind(":file", root.toString); 246 auto app = appender!(Path[])(); 247 foreach (ref a; stmt.get.execute) { 248 app.put(Path(a.peek!string(0))); 249 } 250 251 return app.data; 252 } 253 254 /// Remove all dependencies that have no relation to a root. 255 void cleanup() @trusted { 256 db.run(format!"DELETE FROM %1$s 257 WHERE id NOT IN (SELECT dep_id FROM %2$s)"(depFileTable, 258 depRootTable)); 259 } 260 } 261 262 /// A file that a root is dependent on. 263 struct DepFile { 264 Path file; 265 Checksum64 checksum; 266 SysTime timeStamp; 267 } 268 269 TrackFile toTrackFile(DepFile a) { 270 return TrackFile(a.file, a.checksum, a.timeStamp); 271 } 272 273 /// Primary key in the files table 274 alias FileId = NamedType!(long, Tag!"FileId", long.init, Comparable, Hashable, TagStringable); 275 276 struct TrackFile { 277 Path file; 278 Checksum64 checksum; 279 SysTime timeStamp; 280 } 281 282 struct DbCompileDbTrack { 283 private Miniorm* db; 284 private Database* wrapperDb; 285 286 void put(TrackFile f) { 287 static immutable sql = "INSERT OR REPLACE INTO " ~ compileDbTrack 288 ~ " (path,time_stamp,checksum) VALUES(:path,:ts,:cs)"; 289 290 auto stmt = db.prepare(sql); 291 stmt.get.bind(":path", f.file.toString); 292 stmt.get.bind(":ts", f.timeStamp.toSqliteDateTime); 293 stmt.get.bind(":cs", cast(long) f.checksum.c0); 294 stmt.get.execute; 295 } 296 297 TrackFile get(const Path path) { 298 static immutable sql = "SELECT time_stamp,checksum FROM " 299 ~ compileDbTrack ~ " WHERE path=:path"; 300 auto stmt = db.prepare(sql); 301 stmt.get.bind(":path", path); 302 auto rval = TrackFile(path); 303 foreach (ref a; stmt.get.execute) { 304 rval.timeStamp = a.peek!string(0).fromSqLiteDateTime.toLocalTime; 305 rval.checksum = a.peek!long(1).Checksum64; 306 } 307 return rval; 308 } 309 310 /// Remove old entries to avoid infinite growth of the database. 311 void cleanup(const Duration dropAfter) { 312 import std.datetime : Clock, dur; 313 314 static immutable sql = "DELETE FROM " ~ compileDbTrack 315 ~ " WHERE datetime(time_stamp) < datetime(:older_then)"; 316 317 auto stmt = db.prepare(sql); 318 // two is a magic number that I think is ok. Over two days not that 319 // many files should have been added/removed that the database grow to 320 // Gbyte in size. 321 stmt.get.bind(":older_then", (Clock.currTime - dropAfter).toSqliteDateTime); 322 stmt.get.execute; 323 } 324 }